﻿/*
The contents of this file are subject to the Mozilla Public License
Version 1.1 (the "License"); you may not use this file except in
compliance with the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/

Software distributed under the License is distributed on an "AS IS"
basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
License for the specific language governing rights and limitations
under the License.
*/
package com.jorritdevries.console
{
	import flash.display.DisplayObjectContainer;
	import flash.events.EventDispatcher;

	/**
	 * Console is a utility class used for recording and displaying debug
	 * information as well providing means for textual input usefull for 
	 * debugging an application. On purposes this class does not provide a
	 * graphical interface, so it can be integrated easily into any application
	 * without the obligation to use a specific interface.
	 * 
	 * This class is based on the article by Facundo Matias Carreiro,
	 * found at GameDev.net.
	 * 
	 * @see http://www.gamedev.net/reference/articles/article2179.asp
	 * @example The following example displayes how items are added to the
	 * Console class and how they can be called using the Console.
	 * @author jorrit@jorritdevries.com
	 */
	public class Console extends EventDispatcher
	{
		// Reference to the sole instance of the Console;
		private static var msInstance:Console;
		
		// The parser to parse given commandline calls
		private var mCommandParser:CommandParser;
		// The list of ConsoleItems
		internal var mItemList:Object;
		// The buffer of commands issued, used for repeating commands
		private var mCommandBuffer:Array;
		// The list of commands to parse upon every parseCommandQueue call
		// We use an Array, since it is assumed the queue will remain very small
		private var mCommandQueue:Array;
		// The number of commands stored in the command buffer
		private var mCommandBufferSize:uint;
		// The position we are at in the command buffer. Used to get previous or next
		// commands.
		private var mCommandBufferPos:uint;
		// Do we echo executed commands in the text buffer?
		private var mEchoCommands:Boolean;
		// The default command. This will be called if the last given command
		// is invalid, ie. has not been added or the syntax is incorrect
		internal var mDefaultCommand:Function;
		
		/**
		 * Creates the sole instance of the Console class.
		 * @param enforcer SingletonEnforcer object to prevent this class
		 * from being instantiated.
		 * @private
		 */
		public function Console(enforcer:SingletonEnforcer)
		{
			if (!enforcer)
			{
				throw new Error("Console can not be instantiated.");
			}
			mCommandParser = new CommandParser(this);
			mItemList = new Object();
			// Creates a buffer of 20 elements
			mCommandBuffer = new Array();
			mCommandQueue = new Array();
			mCommandBufferSize = 20;
			mCommandBufferPos = 0;
			mEchoCommands = false;
			mDefaultCommand = null;
			// Add default command
			addItem("/help", "print this help message", ConsoleItemType.FUNCTION, displayUsage);
			setDefaultCommand("/help");
		}
		
		/**
		 * Returns the sole instance of the Console class.
		 * @return The sole instance of the Console class.
		 */
		public static function getInstance():Console
		{
			if (!msInstance)
			{
				msInstance = new Console(new SingletonEnforcer());
			}
			return msInstance;
		}
		
		/**
		 * Sets the default command to be the named ConsoleItem. This item
		 * will be used when the command specified is unknown.
		 * @param name The name of the ConsoleItem to be the default.
		 * @throws An Error when the named ConsoleItem is not
		 * found.
		 */
		public function setDefaultCommand(name:String):void
		{
			if (!mItemList[name])
			{
				throw new Error("NoSuchElementException: An item named '" + name +
					"' has not been added.");
			}
			// Cannot cast to Function
			mDefaultCommand = ConsoleItem(mItemList[name]).any;
		}
		
		/**
		 * Sets the previous command message used as the to be used command in the input
		 * field. Until no new message is sent or the end of the command buffer is
		 * reached, this returns the command previous of the command displayed.
		 */
		public function getPreviousCommand():String
		{
			// Set first given command as default
			var command:String = mCommandBuffer[0];
			
			if (mCommandBufferPos > 0)
			{
				command = mCommandBuffer[mCommandBufferPos - 1];
				mCommandBufferPos--;
			}
			
			return command;
		}
		
		/**
		 * Sets the next command message used as the to be used command in the input
		 * field. Until no new message is sent or the end of the command buffer is
		 * reached, this returns the command next of the command displayed.
		 */
		public function getNextCommand():String
		{
			var command:String = "";
			
			if (mCommandBufferPos < mCommandBuffer.length)
			{
				mCommandBufferPos++;
				command = mCommandBuffer[mCommandBufferPos - 1];
			}
			
			return command;
		}
		
		/**
		 * Adds a command to the list of commands that will be parsed the next
		 * time Console.parseCommandQueue is called.
		 * @param command A String specifying the type of command including a 
		 * space-seperated argument list.
		 */
		public function sendCommand(command:String):void
		{
			mCommandQueue.push(command);
		}
		
		/**
		 * Sends a command to the parser, resulting in immediate execution.
		 * @param command A String specifying the type of command including a 
		 * space-seperated argument list.
		 * @throws A CommandParseException if parsing the command fails.
		 */
		public function sendImmediateCommand(command:String):void
		{
			if (mEchoCommands)
			{
				print("cmd: " + command);
			}
			mCommandParser.parseCommandLine(command);
			addCommand(command);
		}
		
		/**
		 * Parses the command queue and empties it afterwards.
		 * @throws A CommandParseException if parsing the commands fails.
		 */
		public function parseCommandQueue():void
		{
			var command:String;
			for (var i:int = 0; i < mCommandQueue.length; ++i)
			{
				command = mCommandQueue[i];
				if (mEchoCommands)
				{
					print("cmd: " + command);
				}
				mCommandParser.parseCommandLine(command);
				addCommand(command);
			}
			mCommandQueue = new Array();
		}
		
		/**
		 * Set to <code>true</code> to echo (print) the commands being parsed.
		 * <p>An echoed command is preceeded by 'cmd: '.</p>
		 */
		public function setEchoCommands(echo:Boolean):void
		{
			mEchoCommands = echo;
		}
		
		/** Returns whether commands are being echoed or not. */
		public function getEchoCommands():Boolean
		{
			return mEchoCommands;
		}
		
		/** Clears the list of previous send commands. */
		public function clearCommandBuffer():void
		{
			mCommandBuffer = new Array();
		}
		
		/**
		 * Sets the number of commands that will be stored in the command buffer.
		 * If the new size is smaller than the previous size, the oldest commands
		 * will be removed from	the buffer.
		 * @param size The new size of the CommandBuffer
		 */
		public function setCommandBufferSize(size:uint):void
		{
			mCommandBufferSize = size
			var newBuffer:Array = new Array();
			
		}
		
		/**
		 * Obtains the number of commands that will be stored in the command
		 * buffer.
		 */
		public function getCommandBufferSize():uint
		{
			return mCommandBufferSize;
		}
		
		/**
		 * Adds a ConsoleItem to the list which operates on a data member of a class instance.
		 * <p>The rest arguments <code>rest</code> can be used as follows. If the item needs
		 * to call a Function, add only the function, which takes an Array as arguments, to the
		 * given parameter list. If the item needs to address a variable, three arguments need
		 * to be added to the parameter list in the following order:
		 * <pre>
		 * - Object to which the variable belongs
		 * - Name of the variable to address
		 * </pre>
		 * An enumeration of possible types is defined in ConsoleItemType.
		 * @param name The name of the item, must be unique.
		 * @param description A description for the item
		 * @param type An uint specifying the datatype of the variable to address
		 * @param ...rest Extra arguments, see details regarding the usage
		 * @throws An Error when one the given arguments is invalid
		 * @throws An Error when a ConsoleItem with the given name
		 * 	has been added already
		 * @see ConsoleItemType
		 */
		public function addItem(name:String, description:String, type:uint, ...rest):void
		{
			// Check if name is valid
			if (!name)
			{
				throw new Error("IllegalArgumentException: The argument 'name' cannot be empty.");
			}
			// Check if a named item has already been added
			if (mItemList[name])
			{
				throw new Error("DuplicateItemException: An item named '" + name +
					"' has already been added.")
			}
			
			// Check rest arguments
			if (!rest.length)
			{
				throw new Error("IllegalArgumentException: '...rest' arguments must be specified.");
			}
			else if (rest.length > 2)
			{
				throw new Error("IllegalArgumentException: Too many '...rest' arguments were given.");
			}
			else if (type != ConsoleItemType.FUNCTION && rest.length < 2)
			{
				throw new Error("IllegalArgumentException: You must specify two arguments " +
					"for the given type '" + type.toString() + "'.");
			}
			
			var item:ConsoleItem;
			if (type == ConsoleItemType.FUNCTION)
			{
				// Check if the single rest argument is a Function
				if (!(rest[0] is Function))
				{
					throw new Error("IllegalArgumentException: The given '...rest' argument " +
						"must be of type 'Function'.");
				}
				item = new ConsoleItem(name, description, type, rest[0]);
			}
			else
			{
				// Check if the first argument is no a primitive data type...
				if (!rest[0] || typeof(rest[0]) == "number" || typeof(rest[0]) == "string"
					|| typeof(rest[0]) == "boolean" || typeof(rest[0]) == "function"
					|| typeof(rest[0]) == "undefined")					
				{
					throw new Error("IllegalArgumentException: The first '...rest' argument " +
						"cannot be a primitive, a Function or 'null'.");
				}
				// ...and the second argument a String
				if (!(rest[1] is String))
				{
					throw new Error("IllegalArgumentException: The second '...rest' argument " +
						"must be of type 'String'.");
				}
				item = new ConsoleItem(name, description, type, rest[0], rest[1]);
			}
			
			mItemList[name] = item;
		}
		
		/** Removes the named ConsoleItem from the list of items. */
		public function removeItem(name:String):void
		{
			if (mItemList[name])
			{
				delete mItemList[name];
			}
		}
		
		/**
		 * Dispatches an event containing the message so it can be displayed in a window.
		 * On request the message can be displayed using the IDE output window.
		 * @param message The String to print
		 * @param colour The colour to be used to display the message
		 * @param useTrace Flag indicating whether or not the message will displayed through the
		 * 	Flash IDE output window.
		 */
		public function print(message:String, colour:uint = 0x000000, useTrace:Boolean = true):void
		{
			if (useTrace)
			{
				trace(message);
			}
			// Dispatch event
			dispatchEvent(new ConsoleMessageEvent(ConsoleMessageEvent.MESSAGE, message, colour));
		}
		
		/**
		 * Displays a list of all possible commands and their descriptions.
		 * Internally calls the print operation, which will dispatch the messages
		 * created.
		 * @param args Array containing the arguments. 
		 * @see print
		 */
		private function displayUsage(...args):void
		{
			if (args)
			{
				for (var i:int = 0; i < args.length; ++i)
				{
					if (args[i] > "")
					{
						trace("args: " + args[i]);
					}
				}
			}
			print("Usage: command [args]");
			var item:ConsoleItem;
			for (var s:String in mItemList)
			{
				item = ConsoleItem(mItemList[s]);
				print("    " + item.name + " : " + item.description);
			}
		}
		
		/**
		 * Adds a command to the end of the CommandBuffer. If the buffer reaches
		 * a size larger than the set size, the first in the buffer is removed.
		 */
		private function addCommand(command:String):void
		{
			mCommandBuffer.push(command);
			// Remove first command
			if (mCommandBuffer.length > mCommandBufferSize)
			{
				mCommandBuffer.shift();
			}
			mCommandBufferPos = mCommandBuffer.length;
		}
	}
	
}

internal class SingletonEnforcer {}